/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */

/**
 * @fileoverview Date/Time parsing library with locale support
 */

var gadgets = gadgets || {};

gadgets.i18n = gadgets.i18n || {};

/**
 * DateTimeParse is for parsing date in a locale-sensitive manner. It allows
 * user to use any customized patterns to parse date-time string under certain
 * locale. Things varies across locales like month name, weekname, field
 * order, etc.
 *
 * This module is the counter-part of DateTimeFormat. They use the same
 * date/time pattern specification, which is borrowed from ICU/JDK.
 *
 * This implementation could parse partial date/time.
 *
 * Time Format Syntax: To specify the time format use a time pattern string.
 * In this pattern, following letters are reserved as pattern letters, which
 * are defined as the following:
 *
 * <pre>
 * Symbol   Meaning                 Presentation        Example
 * ------   -------                 ------------        -------
 * G        era designator          (Text)              AD
 * y#       year                    (Number)            1996
 * M        month in year           (Text & Number)     July & 07
 * d        day in month            (Number)            10
 * h        hour in am/pm (1~12)    (Number)            12
 * H        hour in day (0~23)      (Number)            0
 * m        minute in hour          (Number)            30
 * s        second in minute        (Number)            55
 * S        fractional second       (Number)            978
 * E        day of week             (Text)              Tuesday
 * D        day in year             (Number)            189
 * a        am/pm marker            (Text)              PM
 * k        hour in day (1~24)      (Number)            24
 * K        hour in am/pm (0~11)    (Number)            0
 * z        time zone               (Text)              Pacific Standard Time
 * Z        time zone (RFC 822)     (Number)            -0800
 * v        time zone (generic)     (Text)              Pacific Time
 * '        escape for text         (Delimiter)         'Date='
 * ''       single quote            (Literal)           'o''clock'
 * </pre>
 *
 * The count of pattern letters determine the format. <p>
 * (Text): 4 or more pattern letters--use full form,
 *         less than 4--use short or abbreviated form if one exists.
 *         In parsing, we will always try long format, then short. <p>
 * (Number): the minimum number of digits. <p>
 * (Text & Number): 3 or over, use text, otherwise use number. <p>
 * Any characters that not in the pattern will be treated as quoted text. For
 * instance, characters like ':', '.', ' ', '#' and '@' will appear in the
 * resulting time text even they are not embraced within single quotes. In our
 * current pattern usage, we didn't use up all letters. But those unused
 * letters are strongly discouraged to be used as quoted text without quote.
 * That's because we may use other letter for pattern in future. <p>
 *
 * Examples Using the US Locale:
 *
 * Format Pattern                         Result
 * --------------                         -------
 * "yyyy.MM.dd G 'at' HH:mm:ss vvvv" ->>  1996.07.10 AD at 15:08:56 Pacific Time
 * "EEE, MMM d, ''yy"                ->>  Wed, July 10, '96
 * "h:mm a"                          ->>  12:08 PM
 * "hh 'o''clock' a, zzzz"           ->>  12 o'clock PM, Pacific Daylight Time
 * "K:mm a, vvv"                     ->>  0:00 PM, PT
 * "yyyyy.MMMMM.dd GGG hh:mm aaa"    ->>  01996.July.10 AD 12:08 PM
 *
 * <p> When parsing a date string using the abbreviated year pattern ("yy"),
 * DateTimeParse must interpret the abbreviated year relative to some
 * century. It does this by adjusting dates to be within 80 years before and 20
 * years after the time the parse function is called. For example, using a
 * pattern of "MM/dd/yy" and a DateTimeParse instance created on Jan 1, 1997,
 * the string "01/11/12" would be interpreted as Jan 11, 2012 while the string
 * "05/04/64" would be interpreted as May 4, 1964. During parsing, only
 * strings consisting of exactly two digits, as defined by {@link
 * java.lang.Character#isDigit(char)}, will be parsed into the default
 * century. Any other numeric string, such as a one digit string, a three or
 * more digit string will be interpreted as its face value.
 *
 * <p> If the year pattern does not have exactly two 'y' characters, the year is
 * interpreted literally, regardless of the number of digits. So using the
 * pattern "MM/dd/yyyy", "01/11/12" parses to Jan 11, 12 A.D.
 *
 * <p> When numeric fields abut one another directly, with no intervening
 * delimiter characters, they constitute a run of abutting numeric fields. Such
 * runs are parsed specially. For example, the format "HHmmss" parses the input
 * text "123456" to 12:34:56, parses the input text "12345" to 1:23:45, and
 * fails to parse "1234". In other words, the leftmost field of the run is
 * flexible, while the others keep a fixed width. If the parse fails anywhere in
 * the run, then the leftmost field is shortened by one character, and the
 * entire run is parsed again. This is repeated until either the parse succeeds
 * or the leftmost field is one character in length. If the parse still fails at
 * that point, the parse of the run fails.
 *
 * <p> Now timezone parsing only support GMT:hhmm, GMT:+hhmm, GMT:-hhmm
 *
 */

/**
 * Construct a DateTimeParse object based on current locale by using
 * the symbol table passed in.
 * @constructor
 */
gadgets.i18n.DateTimeParse = function(symbol) {
    this.symbols_ = symbol;
};

/**
 * Year.
 * @type {number}
 */
gadgets.i18n.DateTimeParse.prototype.year = 0;


/**
 * Month.
 * @type {number}
 */
gadgets.i18n.DateTimeParse.prototype.month = 0;


/**
 * Day of month.
 * @type {number}
 */
gadgets.i18n.DateTimeParse.prototype.dayOfMonth = 0;


/**
 * Hours.
 * @type {number}
 */
gadgets.i18n.DateTimeParse.prototype.hours = 0;


/**
 * Minutes.
 * @type {number}
 */
gadgets.i18n.DateTimeParse.prototype.minutes = 0;


/**
 * Seconds.
 * @type {number}
 */
gadgets.i18n.DateTimeParse.prototype.seconds = 0;


/**
 * Milliseconds.
 * @type {number}
 */
gadgets.i18n.DateTimeParse.prototype.milliseconds = 0;


/**
 * Number of years prior to now that the century used to
 * disambiguate two digit years will begin
 *
 * @type {number}
 */
gadgets.i18n.DateTimeParse.ambiguousYearCenturyStart = 80;

/**
 * Apply a pattern to this Parser. The pattern string will be parsed and saved
 * in "compiled" form.
 * Note: this method is somewhat similar to the pattern parsing methold in
 *       datetimeformat. If you see something wrong here, you might want
 *       to check the other.
 * @param {string} pattern It describes the format of date string that need to
 *     be parsed.
 */
gadgets.i18n.DateTimeParse.prototype.applyPattern = function(pattern) {
    this.patternParts_ = [];
    var inQuote = false;
    var buf = '';

    for (var i = 0; i < pattern.length; i++) {
        var ch = pattern.charAt(i);

    // handle space, add literal part (if exist), and add space part
        if (ch == ' ') {
            if (buf.length > 0) {
                this.patternParts_.push({text: buf, count: 0, abutStart: false});
                buf = '';
            }
            this.patternParts_.push({text: ' ', count: 0, abutStart: false});
            while (i + 1 < pattern.length && pattern.charAt(i + 1) == ' ') {
                i++;
            }
        } else if (inQuote) {
            // inside quote, except '', just copy or exit
            if (ch == '\'') {
                if (i + 1 < pattern.length && pattern.charAt(i + 1) == '\'') {
                    // quote appeared twice continuously, interpret as one quote.
                    buf += ch;
                    ++i;
                } else {
                    // exit quote
                    inQuote = false;
                }
            } else {
                // literal
                buf += ch;
            }
        } else if (gadgets.i18n.DateTimeParse.PATTERN_CHARS_.indexOf(ch) >= 0) {
            // outside quote, it is a pattern char
            if (buf.length > 0) {
                this.patternParts_.push({text: buf, count: 0, abutStart: false});
                buf = '';
            }
            var count = this.getNextCharCount_(pattern, i);
            this.patternParts_.push({text: ch, count: count, abutStart: false});
            i += count - 1;
        } else if (ch == '\'') {
            // Two consecutive quotes is a quote literal, inside or outside of quotes.
            if (i + 1 < pattern.length && pattern.charAt(i + 1) == '\'') {
                buf += "'";
                i++;
            } else {
                inQuote = true;
            }
        } else {
            buf += ch;
        }
    }

    if (buf.length > 0) {
        this.patternParts_.push({text: buf, count: 0, abutStart: false});
    }

    this.markAbutStart_();
};


/**
 * Apply a predefined pattern to this Parser.
 * @param {number} formatType A constant used to identified the predefined
 *     pattern string stored in locale repository.
 * @return {void} nothing
 */
gadgets.i18n.DateTimeParse.prototype.applyStandardPattern = function(formatType)
{
    var pattern;
  // formatType constants are defined in a way so that following resolution is
    // possible.
    if (formatType < 4) {
        pattern = this.symbols_.DATEFORMATS[formatType];
    } else if (formatType < 8) {
        pattern = this.symbols_.TIMEFORMATS[formatType - 4];
    } else if (formatType < 12) {
        pattern = this.symbols_.DATEFORMATS[formatType - 8];
        pattern += ' ';
        pattern += this.symbols_.TIMEFORMATS[formatType - 8];
    } else {
        return this.applyStandardPattern(gadgets.i18n.MEDIUM_DATETIME_FORMAT);
    }
    return this.applyPattern(pattern);
};


/**
 * Parse the given string and fill info into date object. This version does
 * not validate the input.
 * @param {string} text The string being parsed.
 * @param {number} start The position from where parse should begin.
 * @param {Date} date The Date object to hold the parsed date.
 * @return {number} How many characters parser advanced.
 */
gadgets.i18n.DateTimeParse.prototype.parse = function(text, start, date) {
    return this.internalParse_(text, start, date, false);
};


/**
 * Parse the given string and fill info into date object.
 * @param {string} text The string being parsed.
 * @param {number} start The position from where parse should begin.
 * @param {Date} date The Date object to hold the parsed date.
 * @param {boolean} validation If true, input string need to be a valid
 *     date/time string.
 * @return {number} How many characters parser advanced.
 * @private
 */
gadgets.i18n.DateTimeParse.prototype.internalParse_ =
function(text, start, date, validation) {
    var cal = new gadgets.i18n.DateTimeParse.MyDate_();
    var parsePos = [start];

  // For parsing abutting numeric fields. 'abutPat' is the
    // offset into 'pattern' of the first of 2 or more abutting
    // numeric fields. 'abutStart' is the offset into 'text'
    // where parsing the fields begins. 'abutPass' starts off as 0
    // and increments each time we try to parse the fields.
    var abutPat = -1; // If >=0, we are in a run of abutting numeric fields
    var abutStart = 0;
    var abutPass = 0;

    for (var i = 0; i < this.patternParts_.length; ++i) {
        if (this.patternParts_[i].count > 0) {
            if (abutPat < 0 && this.patternParts_[i].abutStart) {
                abutPat = i;
                abutStart = start;
                abutPass = 0;
            }

      // Handle fields within a run of abutting numeric fields. Take
            // the pattern "HHmmss" as an example. We will try to parse
            // 2/2/2 characters of the input text, then if that fails,
            // 1/2/2. We only adjust the width of the leftmost field; the
            // others remain fixed. This allows "123456" => 12:34:56, but
            // "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we
            // try 4/2/2, 3/2/2, 2/2/2, and finally 1/2/2.
            if (abutPat >= 0) {
                // If we are at the start of a run of abutting fields, then
                // shorten this field in each pass. If we can't shorten
                // this field any more, then the parse of this set of
                // abutting numeric fields has failed.
                var count = this.patternParts_[i].count;
                if (i == abutPat) {
                    count -= abutPass;
                    abutPass++;
                    if (count == 0) {
                        // tried all possible width, fail now
                        return 0;
                    }
                }

                if (!this.subParse_(text, parsePos, this.patternParts_[i], count,
                        cal)) {
                    // If the parse fails anywhere in the run, back up to the
                    // start of the run and retry.
                    i = abutPat - 1;
                    parsePos[0] = abutStart;
                    continue;
                }
            }

                // Handle non-numeric fields and non-abutting numeric fields.
            else {
                abutPat = -1;
                if (!this.subParse_(text, parsePos, this.patternParts_[i], 0, cal)) {
                    return 0;
                }
            }
        } else {
            // Handle literal pattern characters. These are any
            // quoted characters and non-alphabetic unquoted
            // characters.
            abutPat = -1;
      // A run of white space in the pattern matches a run
            // of white space in the input text.
            if (this.patternParts_[i].text.charAt(0) == ' ') {
                // Advance over run in input text
                var s = parsePos[0];
                this.skipSpace_(text, parsePos);

        // Must see at least one white space char in input
                if (parsePos[0] > s) {
                    continue;
                }
            } else if (text.indexOf(this.patternParts_[i].text, parsePos[0]) ==
                       parsePos[0]) {
                parsePos[0] += this.patternParts_[i].text.length;
                continue;
            }
      // We fall through to this point if the match fails
            return 0;
        }
    }

  // return progress
    return cal.calcDate_(date, validation) ? parsePos[0] - start : 0;
};

/**
 * Calculate character repeat count in pattern.
 *
 * @param {string} pattern It describes the format of date string that need to
 *     be parsed.
 * @param {number} start the position of pattern character.
 *
 * @return {number} Repeat count.
 * @private
 */
gadgets.i18n.DateTimeParse.prototype.getNextCharCount_ =
function(pattern, start) {
    var ch = pattern.charAt(start);
    var next = start + 1;
    while (next < pattern.length && pattern.charAt(next) == ch) {
        ++next;
    }
    return next - start;
};

/**
 * All acceptable pattern characters.
 * @private
 */
gadgets.i18n.DateTimeParse.PATTERN_CHARS_ = 'GyMdkHmsSEDahKzZv';

/**
 * Pattern characters that specify numerical field.
 * @private
 */
gadgets.i18n.DateTimeParse.NUMERIC_FORMAT_CHARS_ = 'MydhHmsSDkK';

/**
 * Check if the pattern part is a numeric field.
 *
 * @param {Object} part pattern part to be examined.
 *
 * @return {boolean} true if the pattern part is numberic field.
 * @private
 */
gadgets.i18n.DateTimeParse.prototype.isNumericField_ = function(part) {
    if (part.count <= 0) {
        return false;
    }
    var i = gadgets.i18n.DateTimeParse.NUMERIC_FORMAT_CHARS_.indexOf(
            part.text.charAt(0));
    return i > 0 || i == 0 && part.count < 3;
};


/**
 * Identify the start of an abutting numeric fields' run. Taking pattern
 * "HHmmss" as an example. It will try to parse 2/2/2 characters of the input
 * text, then if that fails, 1/2/2. We only adjust the width of the leftmost
 * field; the others remain fixed. This allows "123456" => 12:34:56, but
 * "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we try 4/2/2,
 * 3/2/2, 2/2/2, and finally 1/2/2. The first field of connected numeric
 * fields will be marked as abutStart, its width can be reduced to accomodate
 * others.
 *
 * @private
 */
gadgets.i18n.DateTimeParse.prototype.markAbutStart_ = function() {
    // abut parts are continuous numeric parts. abutStart is the switch
    // point from non-abut to abut
    var abut = false;

    for (var i = 0; i < this.patternParts_.length; i++) {
        if (this.isNumericField_(this.patternParts_[i])) {
            // if next part is not following abut sequence, and isNumericField_
            if (!abut && i + 1 < this.patternParts_.length &&
                this.isNumericField_(this.patternParts_[i + 1])) {
                abut = true;
                this.patternParts_[i].abutStart = true;
            }
        } else {
            abut = false;
        }
    }
};


/**
 * Skip space in the string.
 *
 * @param {string} text input string.
 * @param {Array} pos where skip start, and return back where the skip stops.
 * @private
 */
gadgets.i18n.DateTimeParse.prototype.skipSpace_ = function(text, pos) {
    var m = text.substring(pos[0]).match(/^\s+/);
    if (m) {
        pos[0] += m[0].length;
    }
};

/**
 * Protected method that converts one field of the input string into a
 * numeric field value.
 *
 * @param {string} text the time text to be parsed.
 * @param {Array} pos Parse position.
 * @param {Object} part the pattern part for this field.
 * @param {number} digitCount when > 0, numeric parsing must obey the count.
 * @param {Object} cal MyDate_ object that will hold parsed value.
 *
 * @return {boolean} True if it parses successfully.
 * @private
 */
gadgets.i18n.DateTimeParse.prototype.subParse_ =
function(text, pos, part, digitCount, cal) {
    this.skipSpace_(text, pos);

    var start = pos[0];
    var ch = part.text.charAt(0);

  // parse integer value if it is a numeric field
    var value = -1;
    if (this.isNumericField_(part)) {
        if (digitCount > 0) {
            if ((start + digitCount) > text.length) {
                return false;
            }
            value = this.parseInt_(
                    text.substring(0, start + digitCount), pos);
        } else {
            value = this.parseInt_(text, pos);
        }
    }

    switch (ch) {
        case 'G': // ERA
            cal.era = this.matchString_(text, pos, this.symbols_.ERAS);
            return true;
        case 'M': // MONTH
            return this.subParseMonth_(text, pos, cal, value);
        case 'E':
            return this.subParseDayOfWeek_(text, pos, cal);
        case 'a': // AM_PM
            cal.ampm = this.matchString_(text, pos, this.symbols_.AMPMS);
            return true;
        case 'y': // YEAR
            return this.subParseYear_(text, pos, start, value, part, cal);
        case 'd': // DATE
            cal.day = value;
            return true;
        case 'S': // FRACTIONAL_SECOND
            return this.subParseFractionalSeconds_(value, pos, start, cal);
        case 'h': // HOUR (1..12)
            if (value == 12) {
                value = 0;
            }
        case 'K': // HOUR (0..11)
        case 'H': // HOUR_OF_DAY (0..23)
        case 'k': // HOUR_OF_DAY (1..24)
            cal.hours = value;
            return true;
        case 'm': // MINUTE
            cal.minutes = value;
            return true;
        case 's': // SECOND
            cal.seconds = value;
            return true;

        case 'z': // ZONE_OFFSET
        case 'Z': // TIMEZONE_RFC
        case 'v': // TIMEZONE_GENERIC
            return this.subparseTimeZoneInGMT_(text, pos, cal);
        default:
            return false;
    }
};

/**
 * Parse year field. Year field is special because
 * 1) two digit year need to be resolved.
 * 2) we allow year to take a sign.
 * 3) year field participate in abut processing.
 *
 * @param {string} text the time text to be parsed.
 * @param {Array} pos Parse position.
 * @param {number} start where this field start.
 * @param {number} value integer value of year.
 * @param {Object} part the pattern part for this field.
 * @param {Object} cal MyDate_ object that will hold parsed value.
 *
 * @return {boolean} True if successful.
 * @private
 */
gadgets.i18n.DateTimeParse.prototype.subParseYear_ =
function(text, pos, start, value, part, cal) {
    var ch;
    if (value < 0) {
        //possible sign
        ch = text.charAt(pos[0]);
        if (ch != '+' && ch != '-') {
            return false;
        }
        pos[0]++;
        value = this.parseInt_(text, pos);
        if (value < 0) {
            return false;
        }
        if (ch == '-') {
            value = -value;
        }
    }

  // only if 2 digit was actually parsed, and pattern say it has 2 digit.
    if (ch == null && (pos[0] - start) == 2 && part.count == 2) {
        cal.setTwoDigitYear_(value);
    } else {
        cal.year = value;
    }
    return true;
};

/**
 * Parse Month field.
 *
 * @param {string} text the time text to be parsed.
 * @param {Array} pos Parse position.
 * @param {Object} cal MyDate_ object that will hold parsed value.
 * @param {number} value numeric value if this field is expressed using
 *      numeric pattern, or -1 if not.
 *
 * @return {boolean} True if parsing successful.
 * @private
 */
gadgets.i18n.DateTimeParse.prototype.subParseMonth_ =
function(text, pos, cal, value) {
    // when month is symbols, i.e., MMM or MMMM, value will be -1
    if (value < 0) {
        // Want to be able to parse both short and long forms.
        // Try count == 4 first:
        value = this.matchString_(text, pos, this.symbols_.MONTHS);
        if (value < 0) { // count == 4 failed, now try count == 3
            value = this.matchString_(text, pos, this.symbols_.SHORTMONTHS);
        }
        if (value < 0) {
            return false;
        }
        cal.month = value;
        return true;
    } else {
        cal.month = value - 1;
        return true;
    }
};

/**
 * Parse Day of week field.
 * @param {string} text the time text to be parsed.
 * @param {Array} pos Parse position.
 * @param {Object} cal MyDate_ object that holds parsed value.
 *
 * @return {boolean} True if successful.
 * @private
 */
gadgets.i18n.DateTimeParse.prototype.subParseDayOfWeek_ =
function(text, pos, cal) {
    // Handle both short and long forms.
    // Try count == 4 (DDDD) first:
    var value = this.matchString_(text, pos, this.symbols_.WEEKDAYS);
    if (value < 0) {
        value = this.matchString_(text, pos, this.symbols_.SHORTWEEKDAYS);
    }
    if (value < 0) {
        return false;
    }
    cal.dayOfWeek = value;
    return true;
};

/**
 * Parse fractional seconds field.
 *
 * @param {number} value parsed numberic value.
 * @param {Array} pos current parse position.
 * @param {number} start where this field start.
 * @param {Object} cal MyDate_ object that holds parsed value.
 *
 * @return {boolean} True if successful.
 * @private
 */
gadgets.i18n.DateTimeParse.prototype.subParseFractionalSeconds_ =
function(value, pos, start, cal) {
    // Fractional seconds left-justify
    var len = pos[0] - start;
    cal.milliseconds = len < 3 ? value * Math.pow(10, 3 - len) :
                       Math.round(value / Math.pow(10, len - 3));
    return true;
};

/**
 * Parse GMT type timezone.
 *
 * @param {string} text the time text to be parsed.
 * @param {Array} pos Parse position.
 * @param {Object} cal MyDate_ object that holds parsed value.
 *
 * @return {boolean} True if successful.
 * @private
 */
gadgets.i18n.DateTimeParse.prototype.subparseTimeZoneInGMT_ =
function(text, pos, cal) {
    // First try to parse generic forms such as GMT-07:00. Do this first
    // in case localized DateFormatZoneData contains the string "GMT"
    // for a zone; in that case, we don't want to match the first three
    // characters of GMT+/-HH:MM etc.

    // For time zones that have no known names, look for strings
    // of the form:
    //    GMT[+-]hours:minutes or
    //    GMT[+-]hhmm or
    //    GMT.
    if (text.indexOf('GMT', pos[0]) == pos[0]) {
        pos[0] += 3;  // 3 is the length of GMT
        return this.parseTimeZoneOffset_(text, pos, cal);
    }

  // TODO(shanjian): check for named time zones by looking through the locale
    // data from the DateFormatZoneData strings. should parse both short and long
    // forms.
    // subParseZoneString(text, start, cal);

    // As a last resort, look for numeric timezones of the form
    // [+-]hhmm as specified by RFC 822.  This code is actually
    // a little more permissive than RFC 822.  It will try to do
    // its best with numbers that aren't strictly 4 digits long.
    return this.parseTimeZoneOffset_(text, pos, cal);
};

/**
 * Parse time zone offset.
 *
 * @param {string} text the time text to be parsed.
 * @param {Array} pos Parse position.
 * @param {Object} cal MyDate_ object that holds parsed value.
 *
 * @return {boolean} True if successful.
 * @private
 */
gadgets.i18n.DateTimeParse.prototype.parseTimeZoneOffset_ =
function(text, pos, cal) {
    if (pos[0] >= text.length) {
        cal.tzOffset = 0;
        return true;
    }

    var sign = 1;
    switch (text.charAt(pos[0])) {
        case '-': sign = -1;  // fall through
        case '+': pos[0]++;
    }

  // Look for hours:minutes or hhmm.
    var st = pos[0];
    var value = this.parseInt_(text, pos);
    if (value == 0 && pos[0] == st) {
        return false;
    }

    var offset;
    if (pos[0] < text.length && text.charAt(pos[0]) == ':') {
        // This is the hours:minutes case
        offset = value * 60;
        pos[0]++;
        st = pos[0];
        value = this.parseInt_(text, pos);
        if (value == 0 && pos[0] == st) {
            return false;
        }
        offset += value;
    } else {
        // This is the hhmm case.
        offset = value;
    // Assume "-23".."+23" refers to hours.
        if (offset < 24 && (pos[0] - st) <= 2)
            offset *= 60;
        else
            // todo: this looks questionable, should have more error checking
            offset = offset % 100 + offset / 100 * 60;
    }

    offset *= sign;
    cal.tzOffset = -offset;
    return true;
};

/**
 * Parse a integer string and return integer value.
 *
 * @param {string} text string being parsed.
 * @param {Array} pos parse position.
 *
 * @return {number} Converted integer value.
 * @private
 */
gadgets.i18n.DateTimeParse.prototype.parseInt_ = function(text, pos) {
    var m = text.substring(pos[0]).match(/^\d+/);
    if (!m) {
        return -1;
    }
    pos[0] += m[0].length;
    return parseInt(m[0], 10);
};

/**
 * Attempt to match the text at a given position against an array of strings.
 * Since multiple strings in the array may match (for example, if the array
 * contains "a", "ab", and "abc", all will match the input string "abcd") the
 * longest match is returned.
 *
 * @param {string} text The string to match to.
 * @param {Array} pos parsing position.
 * @param {Array} data The string array that is used to found match from.
 *
 * @return {number} the new start position if matching succeeded; a negative
 *     number indicating matching failure.
 * @private
 */
gadgets.i18n.DateTimeParse.prototype.matchString_ = function(text, pos, data) {
    // There may be multiple strings in the data[] array which begin with
    // the same prefix (e.g., Cerven and Cervenec (June and July) in Czech).
    // We keep track of the longest match, and return that. Note that this
    // unfortunately requires us to test all array elements.
    var bestMatchLength = 0;
    var bestMatch = -1;
    var lower_text = text.substring(pos[0]).toLowerCase();
    for (var i = 0; i < data.length; ++i) {
        var len = data[i].length;
    // Always compare if we have no match yet; otherwise only compare
        // against potentially better matches (longer strings).
        if (len > bestMatchLength &&
            lower_text.indexOf(data[i].toLowerCase()) == 0) {
            bestMatch = i;
            bestMatchLength = len;
        }
    }
    if (bestMatch >= 0) {
        pos[0] += bestMatchLength;
    }
    return bestMatch;
};


/**
 * This class hold the intermediate parsing result. After all fields are
 * consumed, final result will be resolved from this class.
 * @constructor
 * @private
 */
gadgets.i18n.DateTimeParse.MyDate_ = function() {
};

/**
 * 2 digit year special handling. Assuming for example that the
 * defaultCenturyStart is 6/18/1903. This means that two-digit years will be
 * forced into the range 6/18/1903 to 6/17/2003. As a result, years 00, 01, and
 * 02 correspond to 2000, 2001, and 2002. Years 04, 05, etc. correspond
 * to 1904, 1905, etc. If the year is 03, then it is 2003 if the
 * other fields specify a date before 6/18, or 1903 if they specify a
 * date afterwards. As a result, 03 is an ambiguous year. All other
 * two-digit years are unambiguous.
 *
 * @param {number} year 2 digit year value before adjustment.
 * @return {number} disambiguated year
 * @private
 */
gadgets.i18n.DateTimeParse.MyDate_.prototype.setTwoDigitYear_ = function(year)
{
    var now = new Date();
    var defaultCenturyStartYear =
            now.getFullYear() - gadgets.i18n.DateTimeParse.ambiguousYearCenturyStart;
    var ambiguousTwoDigitYear = defaultCenturyStartYear % 100;
    this.ambiguousYear = (year == ambiguousTwoDigitYear);
    year += Math.floor(defaultCenturyStartYear / 100) * 100 +
            (year < ambiguousTwoDigitYear ? 100 : 0);
    return this.year = year;
};

/**
 * Based on the fields set, fill a Date object. For those fields that not
 * set, use the passed in date object's value.
 *
 * @param {Date} date Date object to be filled.
 * @param {boolean} validation If true, input string will be checked to make
 *     sure it is valid.
 *
 * @return {boolean} false if fields specify a invalid date.
 * @private
 */
gadgets.i18n.DateTimeParse.MyDate_.prototype.calcDate_ =
function(date, validation) {
    // year 0 is 1 BC, and so on.
    if (this.era != undefined && this.year != undefined &&
        this.era == 0 && this.year > 0) {
        this.year = -(this.year - 1);
    }

    if (this.year != undefined) {
        date.setFullYear(this.year);
    }

  // The setMonth and setDate logic is a little tricky. We need to make sure
    // day of month is smaller enough so that it won't cause a month switch when
    // setting month. For example, if data in date is Nov 30, when month is set
    // to Feb, because there is no Feb 30, JS adjust it to Mar 2. So Feb 12 will
    // become  Mar 12.
    var org_date = date.getDate();
    date.setDate(1); // every month has a 1st day, this can actually be anything
    // less than 29.

    if (this.month != undefined) {
        date.setMonth(this.month);
    }

    if (this.day != undefined) {
        date.setDate(this.day);
    } else {
        date.setDate(org_date);
    }

    if (this.hours == undefined) {
        this.hours = date.getHours();
    }

  // adjust ampm
    if (this.ampm != undefined && this.ampm > 0) {
        if (this.hours < 12) {
            this.hours += 12;
        }
    }
    date.setHours(this.hours);

    if (this.minutes != undefined) {
        date.setMinutes(this.minutes);
    }

    if (this.seconds != undefined) {
        date.setSeconds(this.seconds);
    }

    if (this.milliseconds != undefined) {
        date.setMilliseconds(this.milliseconds);
    }

  // If validation is needed, verify that the uncalculated date fields
    // match the calculated date fields.  We do this before we set the
    // timezone offset, which will skew all of the dates.
    //
    // Don't need to check the day of week as it is guaranteed to be
    // correct or return false below.
    if (validation &&
        (this.year != undefined && this.year != date.getFullYear() ||
         this.month != undefined && this.month != date.getMonth() ||
         this.dayOfMonth != undefined && this.dayOfMonth != date.getDate() ||
         this.hours >= 24 || this.minutes >= 60 || this.seconds >= 60 ||
         this.milliseconds >= 1000)) {
        return false;
    }

  // adjust time zone
    if (this.tzOffset != undefined) {
        var offset = date.getTimezoneOffset();
        date.setTime(date.getTime() + (this.tzOffset - offset) * 60 * 1000);
    }

  // resolve ambiguous year if needed
    if (this.ambiguousYear) { // the two-digit year == the default start year
        var defaultCenturyStart = new Date();
        defaultCenturyStart.setFullYear(
                defaultCenturyStart.getFullYear() -
                gadgets.i18n.DateTimeParse.ambiguousYearCenturyStart);
        if (date.getTime() < defaultCenturyStart.getTime()) {
            date.setFullYear(defaultCenturyStart.getFullYear() + 100);
        }
    }

  // dayOfWeek, validation only
    if (this.dayOfWeek != undefined) {
        if (this.day == undefined) {
            // adjust to the nearest day of the week
            var adjustment = (7 + this.dayOfWeek - date.getDay()) % 7;
            if (adjustment > 3) {
                adjustment -= 7;
            }
            var orgMonth = date.getMonth();
            date.setDate(date.getDate() + adjustment);

      // don't let it switch month
            if (date.getMonth() != orgMonth) {
                date.setDate(date.getDate() + (adjustment > 0 ? -7 : 7));
            }
        } else if (this.dayOfWeek != date.getDay()) {
            return false;
        }
    }
    return true;
};
